یاد بگیرید چگونه با استفاده از هوک useFormStatus در React، تخمین پیشرفت و پیشبینی زمان اتمام را پیادهسازی کنید و تجربه کاربری را در برنامههای سنگین بهبود بخشید.
تخمین پیشرفت با useFormStatus در React: پیشبینی زمان اتمام
هوک useFormStatus در React که در نسخه ۱۸ معرفی شد، اطلاعات ارزشمندی درباره وضعیت ارسال یک فرم ارائه میدهد. اگرچه این هوک مستقیماً قابلیت تخمین پیشرفت را ندارد، اما میتوانیم با استفاده از ویژگیهای آن و تکنیکهای دیگر، بازخورد معناداری را در طول ارسال فرمهایی که ممکن است زمانبر باشند، به کاربران ارائه دهیم. این پست به بررسی روشهایی برای تخمین پیشرفت و پیشبینی زمان اتمام هنگام استفاده از useFormStatus میپردازد که منجر به تجربهای جذابتر و کاربرپسندتر میشود.
درک useFormStatus
قبل از پرداختن به تخمین پیشرفت، بیایید به سرعت هدف useFormStatus را مرور کنیم. این هوک برای استفاده در داخل یک عنصر <form> طراحی شده است که از پراپ action استفاده میکند. این هوک یک شیء حاوی ویژگیهای زیر را برمیگرداند:
pending: یک مقدار بولی که نشان میدهد آیا فرم در حال ارسال است یا خیر.data: دادههایی که با فرم ارسال شدهاند (در صورت موفقیتآمیز بودن ارسال).method: متد HTTP استفاده شده برای ارسال فرم (مانند 'POST'، 'GET').action: تابعی که به پراپactionفرم پاس داده شده است.error: یک شیء خطا در صورت شکست در ارسال.
در حالی که useFormStatus به ما میگوید که آیا فرم در حال ارسال است، هیچ اطلاعات مستقیمی در مورد پیشرفت ارسال ارائه نمیدهد، به خصوص اگر تابع action شامل عملیات پیچیده یا طولانی باشد.
چالش تخمین پیشرفت
چالش اصلی در این واقعیت نهفته است که اجرای تابع action برای React شفاف نیست. ما ذاتاً نمیدانیم فرآیند چقدر پیش رفته است. این امر به ویژه برای عملیات سمت سرور صادق است. با این حال، میتوانیم از استراتژیهای مختلفی برای غلبه بر این محدودیت استفاده کنیم.
استراتژیهایی برای تخمین پیشرفت
در اینجا چندین رویکرد وجود دارد که میتوانید اتخاذ کنید، که هر کدام مزایا و معایب خاص خود را دارند:
۱. رویدادهای ارسالی از سرور (SSE) یا WebSockets
قویترین راهحل اغلب ارسال بهروزرسانیهای پیشرفت از سرور به کلاینت است. این کار را میتوان با استفاده از موارد زیر انجام داد:
- رویدادهای ارسالی از سرور (SSE): یک پروتکل یکطرفه (سرور به کلاینت) که به سرور اجازه میدهد تا بهروزرسانیها را از طریق یک اتصال HTTP واحد به کلاینت ارسال کند. SSE برای زمانی ایدهآل است که کلاینت فقط نیاز به *دریافت* بهروزرسانیها دارد.
- WebSockets: یک پروتکل ارتباطی دوطرفه که یک اتصال پایدار بین کلاینت و سرور فراهم میکند. WebSockets برای بهروزرسانیهای بیدرنگ در هر دو جهت مناسب است.
مثال (SSE):
سمت سرور (Node.js):
const express = require('express');
const app = express();
app.get('/progress', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress > 100) {
progress = 100;
clearInterval(interval);
res.write(`data: {"progress": ${progress}, "completed": true}\n\n`);
res.end();
} else {
res.write(`data: {"progress": ${progress}, "completed": false}\n\n`);
}
}, 500); // Simulate progress update every 500ms
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
سمت کلاینت (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const eventSource = new EventSource('/progress');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setProgress(data.progress);
if (data.completed) {
eventSource.close();
}
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
توضیح:
- سرور هدرهای مناسب برای SSE را تنظیم میکند.
- سرور بهروزرسانیهای پیشرفت را به عنوان رویدادهای
data:ارسال میکند. هر رویداد یک شیء JSON است که حاویprogressو یک پرچمcompletedاست. - کامپوننت React از
EventSourceبرای گوش دادن به این رویدادها استفاده میکند. - کامپوننت وضعیت (
progress) را بر اساس رویدادهای دریافتی بهروز میکند.
مزایا: بهروزرسانیهای دقیق پیشرفت، بازخورد بیدرنگ.
معایب: نیاز به تغییرات در سمت سرور، پیادهسازی پیچیدهتر.
۲. نظرسنجی (Polling) با یک API Endpoint
اگر نمیتوانید از SSE یا WebSockets استفاده کنید، میتوانید نظرسنجی (polling) را پیادهسازی کنید. کلاینت به صورت دورهای درخواستهایی را برای بررسی وضعیت عملیات به سرور ارسال میکند.
مثال:
سمت سرور (Node.js):
const express = require('express');
const app = express();
// Simulate a long-running task
let taskProgress = 0;
let taskId = null;
app.post('/start-task', (req, res) => {
taskProgress = 0;
taskId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); // Generate a unique task ID
// Simulate background processing
const interval = setInterval(() => {
taskProgress += 10;
if (taskProgress >= 100) {
taskProgress = 100;
clearInterval(interval);
}
}, 500);
res.json({ taskId });
});
app.get('/task-status/:taskId', (req, res) => {
if (req.params.taskId === taskId) {
res.json({ progress: taskProgress });
} else {
res.status(404).json({ message: 'Task not found' });
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
سمت کلاینت (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [taskId, setTaskId] = useState(null);
const startTask = async () => {
const response = await fetch('/start-task', { method: 'POST' });
const data = await response.json();
setTaskId(data.taskId);
};
useEffect(() => {
if (!taskId) return;
const interval = setInterval(async () => {
const response = await fetch(`/task-status/${taskId}`);
const data = await response.json();
setProgress(data.progress);
if (data.progress === 100) {
clearInterval(interval);
}
}, 1000); // Poll every 1 second
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Start Task</button>
{taskId && <p>Progress: {progress}%</p>}
</div>
);
}
export default MyComponent;
توضیح:
- کلاینت با فراخوانی
/start-taskیک وظیفه را شروع میکند و یکtaskIdدریافت میکند. - سپس کلاینت به صورت دورهای
/task-status/:taskIdرا برای دریافت پیشرفت نظرسنجی میکند.
مزایا: پیادهسازی نسبتاً ساده، عدم نیاز به اتصالات پایدار.
معایب: ممکن است دقت کمتری نسبت به SSE/WebSockets داشته باشد، به دلیل فاصله زمانی نظرسنجی تأخیر ایجاد میکند، به دلیل درخواستهای مکرر بار سرور را افزایش میدهد.
۳. بهروزرسانیهای خوشبینانه و روشهای ابتکاری (Heuristics)
در برخی موارد، میتوانید از بهروزرسانیهای خوشبینانه همراه با روشهای ابتکاری برای ارائه یک تخمین منطقی استفاده کنید. به عنوان مثال، اگر در حال آپلود فایل هستید، میتوانید تعداد بایتهای آپلود شده در سمت کلاینت را ردیابی کرده و پیشرفت را بر اساس حجم کل فایل تخمین بزنید.
مثال (آپلود فایل):
import React, { useState } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [file, setFile] = useState(null);
const handleFileChange = (event) => {
setFile(event.target.files[0]);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded * 100) / event.total);
setProgress(percentage);
}
});
xhr.open('POST', '/upload'); // Replace with your upload endpoint
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Upload failed');
};
} catch (error) {
console.error('Upload error:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Upload</button>
</form>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
توضیح:
- کامپوننت از یک شیء
XMLHttpRequestبرای آپلود فایل استفاده میکند. - شنونده رویداد
progressدرxhr.uploadبرای ردیابی پیشرفت آپلود استفاده میشود. - ویژگیهای
loadedوtotalرویداد برای محاسبه درصد تکمیل شده استفاده میشوند.
مزایا: فقط در سمت کلاینت، میتواند بازخورد فوری ارائه دهد.
معایب: دقت به قابلیت اطمینان روش ابتکاری بستگی دارد، ممکن است برای همه انواع عملیات مناسب نباشد.
۴. تقسیم Action به مراحل کوچکتر
اگر تابع action چندین مرحله مجزا را انجام میدهد، میتوانید پس از هر مرحله UI را بهروز کنید تا پیشرفت را نشان دهید. این کار مستلزم تغییر تابع action برای ارائه بهروزرسانیها است.
مثال:
import React, { useState } from 'react';
async function myAction(setProgress) {
setProgress(10);
await someAsyncOperation1();
setProgress(40);
await someAsyncOperation2();
setProgress(70);
await someAsyncOperation3();
setProgress(100);
}
function MyComponent() {
const [progress, setProgress] = useState(0);
const handleSubmit = async () => {
await myAction(setProgress);
};
return (
<div>
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
توضیح:
- تابع
myActionیک callback به نامsetProgressرا میپذیرد. - این تابع وضعیت پیشرفت را در نقاط مختلف در طول اجرای خود بهروز میکند.
مزایا: کنترل مستقیم بر بهروزرسانیهای پیشرفت.
معایب: نیاز به تغییر تابع action دارد، اگر مراحل به راحتی قابل تقسیم نباشند، پیادهسازی آن میتواند پیچیدهتر باشد.
پیشبینی زمان اتمام
هنگامی که بهروزرسانیهای پیشرفت را در اختیار دارید، میتوانید از آنها برای پیشبینی زمان تخمینی باقیمانده استفاده کنید. یک رویکرد ساده این است که زمان صرف شده برای رسیدن به یک سطح پیشرفت معین را ردیابی کرده و برای تخمین زمان کل برونیابی کنید.
مثال (سادهشده):
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
const startTimeRef = useRef(null);
useEffect(() => {
if (progress > 0 && startTimeRef.current === null) {
startTimeRef.current = Date.now();
}
if (progress > 0) {
const elapsedTime = Date.now() - startTimeRef.current;
const estimatedTotalTime = (elapsedTime / progress) * 100;
const remainingTime = estimatedTotalTime - elapsedTime;
setEstimatedTimeRemaining(Math.max(0, remainingTime)); // Ensure non-negative
}
}, [progress]);
// ... (rest of the component and progress updates as described in previous sections)
return (
<div>
<p>Progress: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Estimated Time Remaining: {Math.round(estimatedTimeRemaining / 1000)} seconds</p>
)}
</div>
);
}
export default MyComponent;
توضیح:
- ما زمان شروع را هنگامی که پیشرفت برای اولین بار بهروز میشود، ذخیره میکنیم.
- ما زمان سپری شده را محاسبه کرده و از آن برای تخمین زمان کل استفاده میکنیم.
- ما زمان باقیمانده را با کم کردن زمان سپری شده از زمان کل تخمینی محاسبه میکنیم.
ملاحظات مهم:
- دقت: این یک پیشبینی *بسیار* سادهشده است. شرایط شبکه، بار سرور و عوامل دیگر میتوانند به طور قابل توجهی بر دقت تأثیر بگذارند. تکنیکهای پیچیدهتر، مانند میانگینگیری در فواصل زمانی متعدد، میتوانند دقت را بهبود بخشند.
- بازخورد بصری: به وضوح نشان دهید که زمان یک *تخمین* است. نمایش بازهها (مثلاً «زمان تخمینی باقیمانده: ۵-۱۰ ثانیه») میتواند واقعیتر باشد.
- موارد مرزی: موارد مرزی که در ابتدا پیشرفت بسیار کند است را مدیریت کنید. از تقسیم بر صفر یا نمایش تخمینهای بیش از حد بزرگ خودداری کنید.
ترکیب useFormStatus با تخمین پیشرفت
در حالی که useFormStatus به خودی خود اطلاعات پیشرفت را ارائه نمیدهد، میتوانید از ویژگی pending آن برای فعال یا غیرفعال کردن نشانگر پیشرفت استفاده کنید. برای مثال:
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (Progress estimation logic from previous examples)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (Your form submission logic, including updates to progress)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Submit</button>
{pending && <p>Progress: {progress}%</p>}
</form>
);
}
در این مثال، نشانگر پیشرفت فقط زمانی نمایش داده میشود که فرم در حالت انتظار (pending) باشد (یعنی زمانی که useFormStatus.pending برابر با true است).
بهترین شیوهها و ملاحظات
- اولویتبندی دقت: یک تکنیک تخمین پیشرفت متناسب با نوع عملیات در حال انجام انتخاب کنید. SSE/WebSockets به طور کلی دقیقترین نتایج را ارائه میدهند، در حالی که روشهای ابتکاری ممکن است برای کارهای سادهتر کافی باشند.
- ارائه بازخورد بصری واضح: از نوارهای پیشرفت، اسپینرها یا سایر نشانههای بصری برای نشان دادن اینکه یک عملیات در حال انجام است استفاده کنید. نشانگر پیشرفت و در صورت لزوم، زمان تخمینی باقیمانده را به وضوح برچسبگذاری کنید.
- مدیریت مناسب خطاها: اگر در حین عملیات خطایی رخ داد، یک پیام خطای آموزنده به کاربر نمایش دهید. از باقی ماندن نشانگر پیشرفت در یک درصد خاص خودداری کنید.
- بهینهسازی عملکرد: از انجام عملیاتهای محاسباتی سنگین در ترد UI خودداری کنید، زیرا این امر میتواند بر عملکرد تأثیر منفی بگذارد. از وب ورکرها یا تکنیکهای دیگر برای انتقال کار به تردهای پسزمینه استفاده کنید.
- دسترسپذیری: اطمینان حاصل کنید که نشانگرهای پیشرفت برای کاربران دارای معلولیت قابل دسترس هستند. از ویژگیهای ARIA برای ارائه اطلاعات معنایی در مورد پیشرفت عملیات استفاده کنید. به عنوان مثال، از
aria-valuenow،aria-valueminوaria-valuemaxدر یک نوار پیشرفت استفاده کنید. - بومیسازی (Localization): هنگام نمایش زمان تخمینی باقیمانده، به فرمتهای زمانی مختلف و ترجیحات منطقهای توجه داشته باشید. از کتابخانهای مانند
date-fnsیاmoment.jsبرای فرمتبندی مناسب زمان برای منطقه کاربر استفاده کنید. - بینالمللیسازی (Internationalization): پیامهای خطا و سایر متون باید برای پشتیبانی از چندین زبان بینالمللی شوند. از کتابخانهای مانند
i18nextبرای مدیریت ترجمهها استفاده کنید.
نتیجهگیری
اگرچه هوک useFormStatus در React به طور مستقیم قابلیتهای تخمین پیشرفت را ارائه نمیدهد، اما میتوانید آن را با تکنیکهای دیگر ترکیب کنید تا بازخورد معناداری را در حین ارسال فرم به کاربران ارائه دهید. با استفاده از SSE/WebSockets، نظرسنجی، بهروزرسانیهای خوشبینانه یا تقسیم اقدامات به مراحل کوچکتر، میتوانید تجربهای جذابتر و کاربرپسندتر ایجاد کنید. به یاد داشته باشید که دقت را در اولویت قرار دهید، بازخورد بصری واضح ارائه دهید، خطاها را به شیوهای مناسب مدیریت کنید و عملکرد را بهینه سازید تا تجربهای مثبت برای همه کاربران، صرف نظر از موقعیت مکانی یا پیشینه آنها، تضمین شود.